<?php

namespace Illuminate\Foundation;

use Exception;
use Illuminate\Filesystem\Filesystem;

/**
 * 安装包清单文件
 */
class PackageManifest
{
    /**
     * 本机文件系统
     *
     * @var \Illuminate\Filesystem\Filesystem
     */
    public $files;

    /**
     * 系统根目录
     *
     * @var string
     */
    public $basePath;

    /**
     * vendor 目录路径
     *
     * @var string
     */
    public $vendorPath;

    /**
     * packages.php 文件路径
     *
     * @var string|null
     */
    public $manifestPath;

    /**
     * The loaded manifest array.
     *
     * @var array
     */
    public $manifest;

    /**
     * Create a new package manifest instance.
     *
     * @param  \Illuminate\Filesystem\Filesystem $files
     * @param  string $basePath
     * @param  string $manifestPath
     * @return void
     */
    public function __construct(Filesystem $files, $basePath, $manifestPath)
    {
        $this->files = $files;
        $this->basePath = $basePath;
        $this->manifestPath = $manifestPath;
        $this->vendorPath = $basePath . '/vendor';
    }

    /**
     * 获取所有包提供的 ServiceProvider 类名
     *
     * @return array
     */
    public function providers()
    {
        return collect($this->getManifest())->flatMap(function ($configuration) {
            return (array)($configuration['providers'] ?? []);
        })->filter()->all();
    }

    /**
     * Get all of the aliases for all packages.
     *
     * @return array
     */
    public function aliases()
    {
        return collect($this->getManifest())->flatMap(function ($configuration) {
            return (array)($configuration['aliases'] ?? []);
        })->filter()->all();
    }

    /**
     * Get the current package manifest.
     *
     * @return array
     */
    protected function getManifest()
    {
        if (!is_null($this->manifest)) {
            return $this->manifest;
        }

        if (!file_exists($this->manifestPath)) {
            $this->build();
        }

        $this->files->get($this->manifestPath);

        return $this->manifest = file_exists($this->manifestPath) ?
            $this->files->getRequire($this->manifestPath) : [];
    }

    /**
     * 构建 packages.php 清单文件，并写入磁盘缓存
     *
     * @return void
     */
    public function build()
    {
        $packages = [];

        // 解析 Composer 生成的 installed.json 文件，里面存放的是所有已安装包的 composer.json 文件内容
        if ($this->files->exists($path = $this->vendorPath . '/composer/installed.json')) {
            $packages = json_decode($this->files->get($path), true);
        }

        // 判断用户是否配置了，要忽略所有包的自动发现功能
        $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore());

        $this->write(collect($packages)->mapWithKeys(function ($package) {
            return [$this->format($package['name']) => $package['extra']['laravel'] ?? []];
        })->each(function ($configuration) use (&$ignore) {
            $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []);
        })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) {
            return $ignoreAll || in_array($package, $ignore);
        })->filter()->all());
    }

    /**
     * Format the given package name.
     *
     * @param  string $package
     * @return string
     */
    protected function format($package)
    {
        return str_replace($this->vendorPath . '/', '', $package);
    }

    /**
     * 获取用户自定义的要求忽略自动发现的包名
     *
     * @return array
     */
    protected function packagesToIgnore()
    {
        if (!file_exists($this->basePath . '/composer.json')) {
            return [];
        }

        // 取出 composer.json 文件中，用户自己配置的 extra.laravel.dont-discover 选项
        return json_decode(file_get_contents(
                $this->basePath . '/composer.json'
            ), true)['extra']['laravel']['dont-discover'] ?? [];
    }

    /**
     * 将自动发现的包清单文件数组写入磁盘
     *
     * @param  array $manifest
     * @return void
     *
     * @throws \Exception
     */
    protected function write(array $manifest)
    {
        if (!is_writable(dirname($this->manifestPath))) {
            throw new Exception('The ' . dirname($this->manifestPath) . ' directory must be present and writable.');
        }

        $this->files->replace(
            $this->manifestPath, '<?php return ' . var_export($manifest, true) . ';'
        );
    }
}
